{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Домашнее задание по теме \"Рекомендации на основе содержания\"\n",
    "\n",
    "1. Использовать dataset MovieLens\n",
    "1. Построить рекомендации (регрессия, предсказываем оценку) на фичах:\n",
    "1. TF-IDF на тегах и жанрах\n",
    "1. Средние оценки (+ median, variance, etc.) пользователя и фильма\n",
    "1. Оценить RMSE на тестовой выборке"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "from recommendation_models import ContentBasedMovieRecommendations"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для домашней работы был реализован класс ContentBasedMovieRecommendations. \n",
    "\n",
    "Общий алгоритм работы:\n",
    "\n",
    "- подаем на вход пути к файлам movies.csv, ratings.csv и tags.csv, работа класса строго привязана к формату датасета MovieLens;\n",
    "- указываем идентификатор пользователя из датасета MovieLens, для которого будем строить модель;\n",
    "- с помощью метода fit обучаем модель;\n",
    "- с помощью метода evaluate расчитываем MAE и RMSE регрессии предсказания оценок пользователя;\n",
    "- с помощью метода get_recommendations находим топ фильмов для рекомендации пользователю на основании идентификатора просмотренного фильма.\n",
    "\n",
    "### Подробный алгоритм работы модели рекомендаций\n",
    "\n",
    "#### 1.  Подготовка датафреймов для преобразования\n",
    "\n",
    "Для каждого фильма находится средний и медианный рейтинг всех пользователей, а так же стандартное отклонение общего рейтинга. Если у фильма есть средняя оценка, но нет данных по стандартному отклонению, то такие пропуски в СКО заполняются нулями. \n",
    "\n",
    "Фильмы без оценок пользователей удаляются, так как будут проблемы при получении оценки с помощью регрессии.\n",
    "\n",
    "Для каждого фильма собираются в отдельном столбце все тэги пользователя выбранного пользователя. Если пользователь не оставил ни одного тега фильму, то такой пропуск заполняется значением \"NO_USER_TAG\". Еще обрабатывается столбец с жанром для каждого фильма в базе для возможности передать данные в модель TF-IDF.\n",
    "\n",
    "#### 2.  Обучение модели\n",
    "\n",
    "На первом этапе мы обучаем модель NearestNeighbors. Обучаем ее только на текстовых данных в виде TF-IDF без средних оценок. Если искать соседей в том числе по средним оценкам, то при просмотре пользователем фильма со средним рейтингом 3, подбираться будут тоже аналогичные фильмы с такой же плохой средней оценкой. При этом далеко не факт, что сам пользователь поставит оценку лучше, а не близкую к средней. Для обучения совмещаем данные из тегов и жанров в одно текстовое поле и получаем матрицу TF-IDF, масштабируем данные и обучаем модель NearestNeighbors на всей базе фильмов, кроме тех, которые пользователь уже посмотрел. То есть у них нет оценки пользователя.\n",
    "\n",
    "На втором этапе нам нужно обучить модель регрессии, которая будет предсказывать предположительную оценку выбранного пользователя рекомендуемому фильму. В качестве регрессора выбран SVR. Для более качественного обучения модели нам требуется выбирать пользователей, которые поставили много оценок. Для обучения берем не масштабированные данные TF-IDF с предыдущего шага. Добавляем к матрице средние, медианные оценки и СКО. А так же целевую переменную в виде оценки пользователя к каждому фильму. Удаляем из матрицы фильмы, которым выбранный пользователь не поставил оценку. Далее, разделяем выборку на тестовую и обучающую, проводим стандартизацию датасетов и обучаем нашу модель регрессии. Качество обучения регрессии можно проверить через метод класса evaluate.\n",
    "\n",
    "#### 3.  Построение рекомендаций\n",
    "\n",
    "Для построения рекомендаций используется метод get_recommendations, в котором в качестве параметра передаем идентификатор просмотренного пользователем фильма. \n",
    "\n",
    "Общая идея построения рекомендаций следующая:\n",
    "\n",
    "1. находим большое количество похожих фильмов по тегам и жанрам, например, 100 фильмов;\n",
    "1. добавляем к этой выдаче определенное количество случайных фильмов с высоким средним рейтингом, чтобы пользователь мог попробовать посмотреть немного другие хорошие фильмы;\n",
    "1. для полученной выборки предсказываем оценку пользователя и выбираем топ фильмов с максимальной предсказанной оценкой.\n",
    "1. в итоговый топ может как попасть случайный фильм, так и не попасть. Это зависит от предсказанной оценки пользователя и размера топовой выборки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "movies_csv = 'movies.csv'\n",
    "ratings_csv = 'ratings.csv'\n",
    "tags_csv = 'tags.csv'\n",
    "\n",
    "\"\"\"\n",
    "Параметры\n",
    "---------\n",
    "user_id : int или str\n",
    "  Идентификатор пользователя, для которого будем строить модель. Принимает значение int или 'auto'. D случае \n",
    "  auto пользователь будет выбран автоматически по критерию максимального количества оценок к фильмам\n",
    "movies_csv : str\n",
    "  Путь к файлу movies.csv из датасета MovieLens\n",
    "ratings_csv : str\n",
    "  Путь к файлу ratings.csv из датасета MovieLens\n",
    "tags_csv : str\n",
    "  Путь к файлу tags.csv из датасета MovieLens\n",
    "svr_C : float\n",
    "  Коэффициент регуляризации для регрессора SVR, смотри справку по sklearn.svm.SVR\n",
    "svr_tol : float\n",
    "   Минимальный допуск изменения качества SVR до остановки обучения модели, смотри справку по sklearn.svm.SVR\n",
    "svr_kernel : str\n",
    "  Тип ядра для регрессора SVR, смотри справку по sklearn.svm.SVR\n",
    "svr_deegre : str\n",
    "  Степень полинома для ядра poly, смотри справку по sklearn.svm.SVR\n",
    "recommend_count : int\n",
    "  Количество фильмов для итоговой рекомендации\n",
    "random_recommend_films : int\n",
    "  Количество случайных фильмов с высоким общим рейтингом, которые будут подмешиваться в рекомендации\n",
    "n_neighbors : int\n",
    "  Количество сходных по жанру и тегам фильмов для выдачи рекомендаций. При получении рекомендации\n",
    "  модель выберет указанное количество фильмов, предскажет для них оценку пользователя и выберет топ фильмов \n",
    "  уровню оценки. размер топа определяется в recommend_count.\n",
    "\"\"\"\n",
    "model = ContentBasedMovieRecommendations(user_id='auto', movies_csv=movies_csv, \n",
    "                                         ratings_csv=ratings_csv, tags_csv=tags_csv,\n",
    "                                         svr_C=0.5, svr_tol=1e-7, recommend_count=10, random_recommend_films=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Идентификатор пользователя: 414\n",
      "Кол-во оценок пользователя к фильмам: 2698\n",
      "Всего кол-во фильмов: 9724\n",
      "Кол-во фичей при обучении регрессионной модели: 24\n",
      "Кол-во фильмов при обучении регрессионной модели: 2158\n",
      "==================================================\n",
      "Качество работы регрессионной модели предсказания оценок:\n",
      "\n",
      "MAE: 0.5029865636399479\n",
      "RMSE: 0.6821888532086684\n"
     ]
    }
   ],
   "source": [
    "# Обучаем модель. Можно пробовать менять параметры модели SVR через свойства класса, \n",
    "# при этом подготовка датафрейма для обучения из этапа номер 1 будет проходить только один раз для экономии времени.\n",
    "model.svr_C = 0.1\n",
    "model.svr_tol = 1e-7\n",
    "\"\"\"\n",
    "Параметры\n",
    "---------\n",
    "test_size : float\n",
    "  Размер тестовой выборки для обучения регрессионной модели предсказания оценки пользователя. \n",
    "  Принимает значения от 0 до 1\n",
    "\"\"\"\n",
    "model.fit(test_size=0.2)\n",
    "\n",
    "# Проверяем статистику модели после обучения\n",
    "model.print_stat()\n",
    "\n",
    "print('='*50)\n",
    "# Проверяем качество предсказаний оценки пользователя на регрессионной модели\n",
    "print('Качество работы регрессионной модели предсказания оценок:\\n')\n",
    "model.evaluate()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно из MAE мы можем в среднем ошибаться на половину балла.\n",
    "\n",
    "Найдем рекомендуемые фильмы для пользователя с выбранным идентификатором. \n",
    "Предположим, что пользователь посмотрел фильм с идентификатором 107 и ему нужно выдать рекомендации по этому фильму:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Пользователь посмотрел следующий фильм:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>movieId</th>\n",
       "      <th>title</th>\n",
       "      <th>genres</th>\n",
       "      <th>rating_mean</th>\n",
       "      <th>rating_median</th>\n",
       "      <th>rating_std</th>\n",
       "      <th>user_rating</th>\n",
       "      <th>user_tags</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>107</td>\n",
       "      <td>Muppet Treasure Island (1996)</td>\n",
       "      <td>adventure children comedy musical</td>\n",
       "      <td>3.326923</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0.92674</td>\n",
       "      <td>3.0</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    movieId                          title                             genres  \\\n",
       "95      107  Muppet Treasure Island (1996)  adventure children comedy musical   \n",
       "\n",
       "    rating_mean  rating_median  rating_std  user_rating    user_tags  \n",
       "95     3.326923            3.0     0.92674          3.0  NO_USER_TAG  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ТОП-10 рекомендованных фильмов для просмотра в порядке максимальной предсказанной оценки пользователя:\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>movieId</th>\n",
       "      <th>title</th>\n",
       "      <th>genres</th>\n",
       "      <th>rating_mean</th>\n",
       "      <th>rating_median</th>\n",
       "      <th>rating_std</th>\n",
       "      <th>user_tags</th>\n",
       "      <th>distance</th>\n",
       "      <th>user_rating_predict</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>506</td>\n",
       "      <td>Orlando (1992)</td>\n",
       "      <td>drama fantasy romance</td>\n",
       "      <td>4.500000</td>\n",
       "      <td>4.50</td>\n",
       "      <td>0.707107</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>None</td>\n",
       "      <td>4.246555</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>1066</td>\n",
       "      <td>Shall We Dance (1937)</td>\n",
       "      <td>comedy musical romance</td>\n",
       "      <td>4.500000</td>\n",
       "      <td>4.50</td>\n",
       "      <td>0.577350</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.612685</td>\n",
       "      <td>4.139664</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>73854</td>\n",
       "      <td>Rudolph, the Red-Nosed Reindeer (1964)</td>\n",
       "      <td>adventure animation children fantasy musical</td>\n",
       "      <td>3.833333</td>\n",
       "      <td>4.00</td>\n",
       "      <td>0.763763</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.117975</td>\n",
       "      <td>4.125484</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>6345</td>\n",
       "      <td>Chorus Line, A (1985)</td>\n",
       "      <td>comedy drama musical</td>\n",
       "      <td>4.666667</td>\n",
       "      <td>5.00</td>\n",
       "      <td>0.577350</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.480666</td>\n",
       "      <td>4.097976</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>1566</td>\n",
       "      <td>Hercules (1997)</td>\n",
       "      <td>adventure animation children comedy musical</td>\n",
       "      <td>3.451613</td>\n",
       "      <td>4.00</td>\n",
       "      <td>1.035655</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>3.203599</td>\n",
       "      <td>4.027364</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>44889</td>\n",
       "      <td>Reefer Madness: The Movie Musical (2005)</td>\n",
       "      <td>comedy drama musical</td>\n",
       "      <td>4.750000</td>\n",
       "      <td>4.75</td>\n",
       "      <td>0.353553</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.480666</td>\n",
       "      <td>4.009247</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>8580</td>\n",
       "      <td>Into the Woods (1991)</td>\n",
       "      <td>adventure comedy fantasy musical</td>\n",
       "      <td>5.000000</td>\n",
       "      <td>5.00</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.553087</td>\n",
       "      <td>3.971669</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>945</td>\n",
       "      <td>Top Hat (1935)</td>\n",
       "      <td>comedy musical romance</td>\n",
       "      <td>4.071429</td>\n",
       "      <td>4.00</td>\n",
       "      <td>0.838082</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.612685</td>\n",
       "      <td>3.970729</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>104875</td>\n",
       "      <td>History of Future Folk, The (2012)</td>\n",
       "      <td>adventure comedy musical scifi</td>\n",
       "      <td>4.000000</td>\n",
       "      <td>4.00</td>\n",
       "      <td>0.707107</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.21303</td>\n",
       "      <td>3.969308</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>7888</td>\n",
       "      <td>How to Succeed in Business Without Really Tryi...</td>\n",
       "      <td>comedy musical</td>\n",
       "      <td>4.250000</td>\n",
       "      <td>4.25</td>\n",
       "      <td>1.060660</td>\n",
       "      <td>NO_USER_TAG</td>\n",
       "      <td>4.500174</td>\n",
       "      <td>3.960656</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   movieId                                              title  \\\n",
       "0      506                                     Orlando (1992)   \n",
       "1     1066                              Shall We Dance (1937)   \n",
       "2    73854             Rudolph, the Red-Nosed Reindeer (1964)   \n",
       "3     6345                              Chorus Line, A (1985)   \n",
       "4     1566                                    Hercules (1997)   \n",
       "5    44889           Reefer Madness: The Movie Musical (2005)   \n",
       "6     8580                              Into the Woods (1991)   \n",
       "7      945                                     Top Hat (1935)   \n",
       "8   104875                 History of Future Folk, The (2012)   \n",
       "9     7888  How to Succeed in Business Without Really Tryi...   \n",
       "\n",
       "                                         genres  rating_mean  rating_median  \\\n",
       "0                         drama fantasy romance     4.500000           4.50   \n",
       "1                        comedy musical romance     4.500000           4.50   \n",
       "2  adventure animation children fantasy musical     3.833333           4.00   \n",
       "3                          comedy drama musical     4.666667           5.00   \n",
       "4   adventure animation children comedy musical     3.451613           4.00   \n",
       "5                          comedy drama musical     4.750000           4.75   \n",
       "6              adventure comedy fantasy musical     5.000000           5.00   \n",
       "7                        comedy musical romance     4.071429           4.00   \n",
       "8                adventure comedy musical scifi     4.000000           4.00   \n",
       "9                                comedy musical     4.250000           4.25   \n",
       "\n",
       "   rating_std    user_tags  distance  user_rating_predict  \n",
       "0    0.707107  NO_USER_TAG      None             4.246555  \n",
       "1    0.577350  NO_USER_TAG  4.612685             4.139664  \n",
       "2    0.763763  NO_USER_TAG  4.117975             4.125484  \n",
       "3    0.577350  NO_USER_TAG  4.480666             4.097976  \n",
       "4    1.035655  NO_USER_TAG  3.203599             4.027364  \n",
       "5    0.353553  NO_USER_TAG  4.480666             4.009247  \n",
       "6    0.000000  NO_USER_TAG  4.553087             3.971669  \n",
       "7    0.838082  NO_USER_TAG  4.612685             3.970729  \n",
       "8    0.707107  NO_USER_TAG   4.21303             3.969308  \n",
       "9    1.060660  NO_USER_TAG  4.500174             3.960656  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "model.get_recommendations(movie_id=107)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
